/*
 * 使用netcat进行测试
 * netcat host port
 * netcat 127.0.0.1 9010
 */

#define VERSION     "1.1.0"

#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <limits.h>

#include <getopt.h>

#define BUFFER_SIZE     4096
char buffer[BUFFER_SIZE];

int fd_serial = -1;
int fd_listen_socket = -1;
int fd_accept_socket = -1;

char* str_serial;
char* str_baudrate;
char* str_listen_port;
char* str_debug_level;
int baudrate;
int listen_port;
int run_once = 0;
int debug_level = 1;

char* const short_options = "s:b:p:d:1";
struct option long_options[] = {
    { "serial",         required_argument, NULL, 's' },
    { "baudrate",       required_argument, NULL, 'b' },
    { "listem-port",    required_argument, NULL, 'p' },
    { "run-once",       no_argument,       NULL, '1' },
    { "debug",          required_argument, NULL, 'd' },
    { 0, 0, 0, 0},
};

#define debug(level, args...) \
    do { \
        if(debug_level >= (level)) \
        {printf(args); fflush(stdout); } \
    }while(0)

static int parse_options(int argc, char *argv[])
{
    int c;

    while((c = getopt_long (argc, argv, short_options, long_options, NULL)) != -1)
    {
        switch (c)
        {
            case 's':
                str_serial = optarg;
                break;

            case 'b':
                str_baudrate = optarg;
                break;

            case 'p':
                str_listen_port = optarg;
                break;
            case 'd':
                str_debug_level = optarg;
                break;

            case '1':
                run_once = 1;
                break;
        }
    }

    if(str_serial == NULL)
    {
        str_serial = "/dev/ttyS0";
    }

    if(str_baudrate == NULL)
    {
        str_baudrate = "115200";
    }

    if(str_listen_port == NULL)
    {
        str_listen_port = "9010";
    }

    if(str_debug_level == NULL)
    {
        str_debug_level = "1";
    }

    baudrate = strtol(str_baudrate, NULL, 10);
    if(baudrate == 0 || baudrate == INT_MIN || baudrate == INT_MAX)
    {
        fprintf(stderr, "Invalid baudrate value \"%s\"\n", str_baudrate);
        return -EINVAL;
    }

    listen_port = strtol(str_listen_port, NULL, 10);
    if(listen_port == 0 || listen_port == INT_MIN || listen_port == INT_MAX)
    {
        fprintf(stderr, "Invalid listen port value \"%s\"\n", str_listen_port);
        return -EINVAL;
    }

    debug_level = strtol(str_debug_level, NULL, 10);
    if(debug_level == INT_MIN || debug_level == INT_MAX)
    {
        fprintf(stderr, "Invalid debug level value \"%s\"\n", str_debug_level);
        return -EINVAL;
    }

    return 0;
}

static int open_serial(void)
{
    fd_serial = open(str_serial, O_RDWR);
    if(fd_serial == -1)
    {
        perror("open");
        return -errno;
    }

    int retval;
    struct termios attr;
    retval = tcgetattr(fd_serial, &attr);
    if(retval == -1)
    {
        perror("tcgetattr");
        return -errno;
    }

    const int speed_arr[] = {
            B230400, B115200, B57600,
            B38400,  B19200,  B9600, };

    const int name_arr[] = {
            230400, 115200, 57600,
            38400,  19200,  9600, };

    int i;
    for(i = 0;  i < sizeof(speed_arr) / sizeof(speed_arr[0]);  i++)
    {
        if(baudrate == name_arr[i])
        {
            cfsetispeed(&attr, speed_arr[i]);
            cfsetospeed(&attr, speed_arr[i]);
            break;
        }
    }
    if(i == sizeof(speed_arr) / sizeof(speed_arr[0]))
    {
        fprintf(stderr, "Invalid baudrate %d\n", baudrate);
        return -EINVAL;
    }

    attr.c_cflag &= ~CSIZE;
    attr.c_cflag |= CS8; /* 8 data bits */

    attr.c_cflag &= ~PARENB;   /* Clear parity enable */
    attr.c_iflag &= ~INPCK;     /* Enable parity checking */

    attr.c_cflag &= ~CSTOPB;    /* 1 stop bit */

    attr.c_lflag  &= ~(ICANON | ECHO | ECHOE | ISIG);
    attr.c_oflag  &= ~OPOST;

    tcflush(fd_serial, TCIFLUSH);

    retval = tcsetattr(fd_serial, TCSANOW, &attr);
    if(retval == -1)
    {
        perror("tcsetattr");
        return -errno;
    }

    tcflush(fd_serial, TCIFLUSH);

    debug(1, "open serial port %s with %d 8N1\n", str_serial, baudrate);

    return 0;
}

static int create_server(void)
{
    fd_listen_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(fd_listen_socket == -1)
    {
        perror("socket");
        return -errno;
    }

    struct sockaddr_in local_addr;
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = PF_INET;
    local_addr.sin_addr.s_addr = INADDR_ANY;
    local_addr.sin_port = htons(listen_port);

    int retval;
    retval = bind(fd_listen_socket,
            (struct sockaddr*)&local_addr,
            sizeof(struct sockaddr_in));
    if(retval == -1)
    {
        perror("bind");
        return -errno;
    }

    debug(1, "server bind to %d\n", listen_port);

    retval = listen(fd_listen_socket, 1);
    if(retval == -1)
    {
        perror("listen");
        return -errno;
    }

    debug(1, "listening...\n");

    return 0;
}


static int accept_client(void)
{
    struct sockaddr_in remote_addr;
    socklen_t addrlen = sizeof(struct sockaddr_in);

    if(fd_accept_socket != -1)
    {
        close(fd_accept_socket);
    }

    fd_accept_socket = accept(fd_listen_socket,
            (struct sockaddr*)&remote_addr,
            &addrlen);
    if(fd_accept_socket == -1)
    {
        perror("accept");
        return -errno;
    }

    debug(1, "connect from %s:%d\n",
            inet_ntoa(remote_addr.sin_addr),
            ntohs(remote_addr.sin_port));

    return 0;
}


static void main_loop(void)
{
    int ret;

    fd_set read_fds;
    int fd_max = fd_serial > fd_accept_socket ? fd_serial : fd_accept_socket;
    int buffer_count = 0;

    for(;;)
    {
        FD_ZERO(&read_fds);
        FD_SET(fd_serial, &read_fds);
        FD_SET(fd_accept_socket, &read_fds);

        ret = select(fd_max + 1, &read_fds, NULL, NULL, NULL);
        if(ret == -1)
        {
            perror("select");
            break;
        }

        if(ret == 0)
        {
            fprintf(stderr, "ret == 0, maybe a bug\n");
            break;
        }

        if(FD_ISSET(fd_accept_socket, &read_fds))
        {
            debug(1, "remote peer disconnect\n");
            break;
        }

        if(FD_ISSET(fd_serial, &read_fds))
        {
            ret = read(fd_serial, &buffer[buffer_count], BUFFER_SIZE - buffer_count);
            if(ret == -1)
            {
                perror("read");
                break;
            }
            if(ret == 0)
            {
                fprintf(stderr, "ret == 0, maybe a bug");
                break;
            }

            buffer_count += ret;
            if(buffer[buffer_count - 1] == '\n' || buffer_count == BUFFER_SIZE)
            {
                buffer[buffer_count] = '\0';
                debug(2, buffer);

                ret = write(fd_accept_socket, buffer, buffer_count);
                if(ret == -1)
                {
                    perror("write");
                    break;
                }
                buffer_count = 0;
            }
        }
    }
}

static void clean(void)
{
    if(fd_serial != -1)
    {
        close(fd_serial);
        fd_serial = -1;
    }

    if(fd_listen_socket != -1)
    {
        close(fd_listen_socket);
        fd_listen_socket = -1;
    }

    if(fd_accept_socket != -1)
    {
        close(fd_accept_socket);
        fd_accept_socket = -1;
    }
}

int main(int argc, char *argv[])
{
    int retval;

    debug(1, "serial-server-" VERSION "\n");
    debug(1, "Report bugs to mingdu.zheng@gmail.com\n");
    debug(1, "GDB Server Extender home page: http://gdbx.zoomdy.org\n");

    retval = parse_options(argc, argv);
    if(retval != 0)
    {
        clean();
        return EXIT_FAILURE;
    }

    retval = open_serial();
    if(retval != 0)
    {
        clean();
        return EXIT_FAILURE;
    }

    retval = create_server();
    if(retval != 0)
    {
        clean();
        return EXIT_FAILURE;
    }

    do
    {
        retval = accept_client();
        if(retval != 0)
        {
            clean();
            return EXIT_FAILURE;
        }

        main_loop();
    }
    while(run_once == 0);

    clean();
    return EXIT_SUCCESS;
}
